<?php declare(strict_types=1);
/**
 * @author      xianganyall <xianganyall@gmail.com>
 * @copyright   2023-2025 owner
 **/

namespace Srv\Libs\Plugins\Apple;

use Exception;
use Srv\Conf\ConfModel\AppleAuthConf;
use Srv\Libs\Common\CommFile;
use Srv\Libs\Common\CommJson;
use Srv\Libs\Common\CommNet;
use Srv\Libs\Common\CommTime;
use Srv\Libs\ConfModel\NetworkConf;
use Srv\Libs\Frame\Conf;
use Srv\Libs\Frame\ControllerAbstract;
use Srv\Libs\Plugins\Jwt\JWT;
use Srv\Libs\Storage\DataRedis;

final class AppleStoreConnect
{
    private string $appleBundleId                   = '';
    private string $appId                           = '';
    private string $authIss                         = '';
    private string $authKeyId                       = '';
    private string $authKeyFile                     = '';
    private string $rootCaCerFile                   = '';
    private string $appStoreConnectUrl              = 'https://api.appstoreconnect.apple.com';
    private ?DataRedis $CcAuto                      = null;             // CcAuto
    private const APPLE_STORE_PREFIX                = 'AP_STORE_';      // 缓存令牌前缀

    /**
     * @param AppleAuthConf $AppleAuthConf
     * __construct
     */
    public function __construct(AppleAuthConf $AppleAuthConf)
    {
        $this->appleBundleId        = $AppleAuthConf->getAppleBundleId();
        $this->appId                = $AppleAuthConf->getAppId();
        $this->authIss              = $AppleAuthConf->getIssId();
        $this->authKeyId            = $AppleAuthConf->getKeyId();
        $this->authKeyFile          = $AppleAuthConf->getAuthKeyFile();
        $this->rootCaCerFile        = $AppleAuthConf->getRootCaCerFile();
        $this->CcAuto               = ControllerAbstract::getCacheAuto();
    }

    /**
     * @return array|false
     * getAppList
     */
    public function getAppList():array
    {
        // https://developer.apple.com/documentation/appstoreconnectapi/list_apps
        $url            = $this->appStoreConnectUrl.'/v1/apps';
        $jwtToken       = $this->getJwtToken([$this->genScope('GET', $url)]);
        $param          = ['TIMEOUT' => 10, 'METHOD' => 'GET', 'AUTH_TYPE' => 'Bearer', 'PASS' => $jwtToken];
        $NetworkConf    = Conf::getNetworkConfGroup()->getByGroupName('auto');
        if($NetworkConf instanceof NetworkConf) $param = array_merge($param, $NetworkConf->getProxyParam(true, true));
        $tryNum         = 2;
        do{
            $urlInfo        = CommNet::getUrlInfo($url, $param);
            $httpCode       = intval($urlInfo['http_code']??-1);
            if($httpCode >= 400 && $httpCode <= 599) break;
            if($httpCode === 200){
                $bodyData   = CommJson::decodeArray(trim($urlInfo['body']??''));
                if(isset($bodyData['data']) && is_array($bodyData['data'])) return $bodyData['data'];
                break;
            }
        }while(--$tryNum >= 0);
        return [];
    }

    /**
     * @param string $appId
     * @return array
     * getAppStoreVersionsList
     */
    public function getAppStoreVersionsList(string $appId):array
    {
        if(strlen($appId) < 6 || preg_match('/^\d+$/', $appId) < 1) return [];
        // https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_store_versions_for_an_app
        $url            = $this->appStoreConnectUrl.'/v1/apps/'.$appId.'/appStoreVersions';
        $jwtToken       = $this->getJwtToken([$this->genScope('GET', $url)]);
        $param          = ['TIMEOUT' => 10, 'METHOD' => 'GET', 'AUTH_TYPE' => 'Bearer', 'PASS' => $jwtToken];
        $NetworkConf    = Conf::getNetworkConfGroup()->getByGroupName('auto');
        if($NetworkConf instanceof NetworkConf) $param = array_merge($param, $NetworkConf->getProxyParam(true, true));
        $tryNum         = 2;
        do{
            $urlInfo        = CommNet::getUrlInfo($url, $param);
            $httpCode       = intval($urlInfo['http_code']??-1);
            if($httpCode >= 400 && $httpCode <= 599) break;
            if($httpCode === 200){
                $bodyData   = CommJson::decodeArray(trim($urlInfo['body']??''));
                if(isset($bodyData['data']) && is_array($bodyData['data'])) return $bodyData['data'];
                break;
            }
        }while(--$tryNum >= 0);
        return [];
    }

    /**
     * @param string $appId
     * @return array
     * getInAppPurchasesV2List
     */
    public function getInAppPurchasesV2List(string $appId):array
    {
        if(strlen($appId) < 6 || preg_match('/^\d+$/', $appId) < 1) return [];
        // https://developer.apple.com/documentation/appstoreconnectapi/list_all_in-app_purchases_for_an_app
        $url            = $this->appStoreConnectUrl.'/v1/apps/'.$appId.'/inAppPurchasesV2';
        $jwtToken       = $this->getJwtToken([$this->genScope('GET', $url)]);
        $param          = ['TIMEOUT' => 10, 'METHOD' => 'GET', 'AUTH_TYPE' => 'Bearer', 'PASS' => $jwtToken];
        $NetworkConf    = Conf::getNetworkConfGroup()->getByGroupName('auto');
        if($NetworkConf instanceof NetworkConf) $param = array_merge($param, $NetworkConf->getProxyParam(true, true));
        $tryNum         = 2;
        do{
            $urlInfo        = CommNet::getUrlInfo($url, $param);
            $httpCode       = intval($urlInfo['http_code']??-1);
            if($httpCode >= 400 && $httpCode <= 599) break;
            if($httpCode === 200){
                $bodyData   = CommJson::decodeArray(trim($urlInfo['body']??''));
                if(isset($bodyData['data']) && is_array($bodyData['data'])) return $bodyData['data'];
                break;
            }
        }while(--$tryNum >= 0);
        return [];
    }

    /**
     * @param string $iapId
     * @return array
     * getInAppPurchaseLocalizations
     */
    public function getInAppPurchaseLocalizations(string $iapId):array
    {
        if(strlen($iapId) < 6 || preg_match('/^\d+$/', $iapId) < 1) return [];
        // https://developer.apple.com/documentation/appstoreconnectapi/list_all_localizations_for_an_in-app_purchase
        $url            = $this->appStoreConnectUrl.'/v2/inAppPurchases/'.$iapId.'/inAppPurchaseLocalizations';
        $jwtToken       = $this->getJwtToken([$this->genScope('GET', $url)]);
        $param          = ['TIMEOUT' => 10, 'METHOD' => 'GET', 'AUTH_TYPE' => 'Bearer', 'PASS' => $jwtToken];
        $NetworkConf    = Conf::getNetworkConfGroup()->getByGroupName('auto');
        if($NetworkConf instanceof NetworkConf) $param = array_merge($param, $NetworkConf->getProxyParam(true, true));
        $tryNum         = 2;
        do{
            $urlInfo        = CommNet::getUrlInfo($url, $param);
            $httpCode       = intval($urlInfo['http_code']??-1);
            if($httpCode >= 400 && $httpCode <= 599) break;
            if($httpCode === 200){
                $bodyData   = CommJson::decodeArray(trim($urlInfo['body']??''));
                if(isset($bodyData['data']) && is_array($bodyData['data'])) return $bodyData['data'];
                break;
            }
        }while(--$tryNum >= 0);
        return [];
    }

    /**
     * @param string $iapId
     * @param string $baseTerritory
     * @param string $priceTier
     * @param bool $isManualMore
     * @return bool
     * getBaseTerritory
     */
    public function getBaseTerritory(string $iapId, string &$baseTerritory, string &$priceTier, bool &$isManualMore):bool
    {
        $baseTerritory  = '';
        $priceTier      = '';
        $isManualMore   = false;
        if(strlen($iapId) < 6 || preg_match('/^\d+$/', $iapId) < 1) return false;
        $url            = $this->appStoreConnectUrl.'/v1/inAppPurchasePriceSchedules/'.$iapId.'?include=baseTerritory,manualPrices';
        $jwtToken       = $this->getJwtToken([$this->genScope('GET', $url)]);
        $param          = ['TIMEOUT' => 10, 'METHOD' => 'GET', 'AUTH_TYPE' => 'Bearer', 'PASS' => $jwtToken];
        $NetworkConf    = Conf::getNetworkConfGroup()->getByGroupName('auto');
        if($NetworkConf instanceof NetworkConf) $param = array_merge($param, $NetworkConf->getProxyParam(true, true));
        $tryNum         = 2;
        do{
            $urlInfo        = CommNet::getUrlInfo($url, $param);
            $httpCode       = intval($urlInfo['http_code']??-1);
            if($httpCode >= 400 && $httpCode <= 599) break;
            if($httpCode === 200){
                $bodyData                   = CommJson::decodeArray(trim($urlInfo['body']??''));
                if(isset($bodyData['data']) && is_array($bodyData['data'])){
                    $baseTerritory          = strval($bodyData['data']['relationships']['baseTerritory']['data']['id']??'');
                    if(strlen($baseTerritory) < 1) return false;
                    $manualTotal            = intval($bodyData['data']['relationships']['manualPrices']['meta']['paging']['total']??0);
                    $manualData             = $bodyData['data']['relationships']['manualPrices']['data']??[];
                    if(is_array($manualData)){
                        $manualDataSize     = count($manualData);
                        if($manualDataSize > 0) foreach ($manualData as $manualDataItem){
                            $manualIdData   = CommJson::decodeArray(base64_decode(strval($manualDataItem['id']??'')));
                            $manualIds      = strval($manualIdData['s']??'');
                            $manualIdt      = strval($manualIdData['t']??'');
                            $manualIdp      = strval($manualIdData['p']??'');
                            if($manualIds !== $iapId || strlen($manualIdt) < 1 || strlen($manualIdp) < 1) continue;
                            if($baseTerritory === $manualIdt){
                                $priceTier  = $manualIdp;
                                break;
                            }
                        }
                        $isManualMore       = $manualDataSize < $manualTotal;
                    }
                }
                break;
            }
        }while(--$tryNum >= 0);
        return strlen($priceTier) > 0;
    }

    /**
     * @param string $iapId
     * @param string $baseTerritory
     * @param string $priceTier
     * @return bool
     * getManualPrices
     */
    public function getManualPrices(string $iapId, string $baseTerritory, string &$priceTier):bool
    {
        $priceTier      = '';
        if(strlen($iapId) < 6 || preg_match('/^\d+$/', $iapId) < 1 || strlen($baseTerritory) < 1) return false;
        $url            = $this->appStoreConnectUrl.'/v1/inAppPurchasePriceSchedules/'.$iapId.'/manualPrices?limit=200';
        $jwtToken       = $this->getJwtToken([$this->genScope('GET', $url)]);
        $param          = ['TIMEOUT' => 10, 'METHOD' => 'GET', 'AUTH_TYPE' => 'Bearer', 'PASS' => $jwtToken];
        $NetworkConf    = Conf::getNetworkConfGroup()->getByGroupName('auto');
        if($NetworkConf instanceof NetworkConf) $param = array_merge($param, $NetworkConf->getProxyParam(true, true));
        $tryNum         = 5;
        do{
            $urlInfo        = CommNet::getUrlInfo($url, $param);
            $httpCode       = intval($urlInfo['http_code']??-1);
            if($httpCode >= 400 && $httpCode <= 599) break;
            if($httpCode === 200){
                $bodyData                   = CommJson::decodeArray(trim($urlInfo['body']??''));
                $manualTotal                = intval($bodyData['meta']['paging']['total']??0);
                if(isset($bodyData['data']) && is_array($bodyData['data'])){
                    $manualData             = $bodyData['data']??[];
                    $manualDataSize         = count($manualData);
                    if($manualDataSize > 0) foreach ($manualData as $manualDataItem){
                        $manualIdData       = CommJson::decodeArray(base64_decode(strval($manualDataItem['id']??'')));
                        $manualIds          = strval($manualIdData['s']??'');
                        $manualIdt          = strval($manualIdData['t']??'');
                        $manualIdp          = strval($manualIdData['p']??'');
                        if($manualIds !== $iapId || strlen($manualIdt) < 1 || strlen($manualIdp) < 1) continue;
                        if($baseTerritory === $manualIdt){
                            $priceTier      = $manualIdp;
                            break;
                        }
                    }
                    if($manualDataSize < $manualTotal && strlen($priceTier) < 1){
                        $nextUrl            = strval($bodyData['links']['next']??'');
                        if(strlen($nextUrl) > 0){
                            $url            = $nextUrl;
                            continue;
                        }
                    }
                }
                break;
            }
        }while(--$tryNum >= 0);
        return strlen($priceTier) > 0;
    }

    /**
     * @param string $iapId
     * @param string $baseTerritory
     * @param string $priceTier
     * @param string $customerPrice
     * @param string $proceeds
     * @return bool
     * getPricePoints
     */
    public function getPricePoints(string $iapId, string &$baseTerritory, string &$priceTier, string &$customerPrice, string &$proceeds):bool
    {
        $baseTerritory      = '';
        $priceTier          = '';
        $customerPrice      = '';
        $proceeds           = '';
        if(strlen($iapId) < 6 || preg_match('/^\d+$/', $iapId) < 1) return false;
        $isManualMore       = false;
        if(!$this->getBaseTerritory($iapId, $baseTerritory, $priceTier, $isManualMore) || strlen($baseTerritory) < 1) return false;
        // 查找手动配置
        if(strlen($priceTier) < 1 && $isManualMore && !$this->getManualPrices($iapId, $baseTerritory, $priceTier)) return false;
        if(strlen($priceTier) < 1) return false;
        // https://developer.apple.com/documentation/appstoreconnectapi/list_all_price_points_for_an_in-app_purchase
        $url            = $this->appStoreConnectUrl.'/v2/inAppPurchases/'.$iapId.'/pricePoints?filter[territory]='.$baseTerritory.'&filter[priceTier]='.$priceTier.'&limit=1000';
        $jwtToken       = $this->getJwtToken([$this->genScope('GET', $url)]);
        $param          = ['TIMEOUT' => 10, 'METHOD' => 'GET', 'AUTH_TYPE' => 'Bearer', 'PASS' => $jwtToken];
        $NetworkConf    = Conf::getNetworkConfGroup()->getByGroupName('auto');
        if($NetworkConf instanceof NetworkConf) $param = array_merge($param, $NetworkConf->getProxyParam(true, true));
        $tryNum         = 2;
        do{
            $urlInfo        = CommNet::getUrlInfo($url, $param);
            $httpCode       = intval($urlInfo['http_code']??-1);
            if($httpCode >= 400 && $httpCode <= 599) break;
            if($httpCode === 200){
                $bodyData                   = CommJson::decodeArray(trim($urlInfo['body']??''));
                if(isset($bodyData['data']) && is_array($bodyData['data'])){
                    $priceData              = $bodyData['data']??[];
                    $priceDataSize          = count($priceData);
                    if($priceDataSize > 0) foreach ($priceData as $priceDataItem){
                        $priceIdData        = CommJson::decodeArray(base64_decode(strval($priceDataItem['id']??'')));
                        $priceIds           = strval($priceIdData['s']??'');
                        $priceIdt           = strval($priceIdData['t']??'');
                        $priceIdp           = strval($priceIdData['p']??'');
                        if($priceIds !== $iapId || strlen($priceIdt) < 1 || strlen($priceIdp) < 1) continue;
                        if($baseTerritory === $priceIdt && $priceTier === $priceIdp){
                            $customerPrice  = strval($priceDataItem['attributes']['customerPrice']??'');
                            $proceeds       = strval($priceDataItem['attributes']['proceeds']??'');
                            break;
                        }
                    }
                }
                break;
            }
        }while(--$tryNum >= 0);
        return strlen($customerPrice) > 0;
    }

    /**
     * @param string $method
     * @param string $url
     * @return string
     * genScope
     */
    private function genScope(string $method, string $url):string
    {
        $method         = strtoupper($method);
        if(!in_array($method, ['GET', 'POST', 'PATCH', 'DELETE'], true)) return '';
        if(preg_match('/^https?:\/\/[^\/]+(.*)$/i', $url, $match) < 1) return '';
        return $method.' '.$match[1];
    }

    /**
     * @return string
     * getJwtToken
     */
    private function getJwtToken(array $scopeList):string
    {
        // 生成 JWT https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests
        $privateKey     = '';
        if(!CommFile::getContent($this->authKeyFile, $privateKey)) return '';
        $cacheKey       = self::APPLE_STORE_PREFIX.md5($privateKey.md5(CommJson::encodeArray($scopeList)));
        $cacheStr       = $this->CcAuto->get($cacheKey);
        if(is_string($cacheStr) && strlen($cacheStr) > 32) return $cacheStr;
        $currTime       = CommTime::getTimeStamp();
        $expTime        = 1200;
        $payload        = [
            'iss'   => $this->authIss,
            'iat'   => $currTime - 60,
            'exp'   => $currTime + $expTime,    // 不能超过20分钟,特例除外
            'aud'   => 'appstoreconnect-v1',
            'scope' => $scopeList,
        ];
        try{
            $jwtToken   = JWT::encode($payload, $privateKey, 'ES256', $this->authKeyId, []);
            if(strlen($jwtToken) > 32) $this->CcAuto->set($cacheKey, $jwtToken, max($expTime - 60, 1));
        }catch (Exception $Exception){
            $jwtToken   = '';
        }
        return $jwtToken;
    }
}